import type { AbortExt, Action, Computed, Rec } from '../'
import {
  action,
  computed,
  deatomize,
  entries,
  isAtom,
  isLinkedListAtom,
  isObject,
  named,
  withAbort,
  withMemo,
} from '../'
import type { FieldError, FieldFocus } from './reatomField'
import { type FieldAtom, fieldInitFocus, isFieldAtom } from './reatomField'
import type {
  FormFieldArrayAtom,
  FormFieldElement,
  FormFields,
  FormInitState,
  FormPartialState,
  FormState,
} from './reatomForm'

export interface FieldSetFieldError extends FieldError {
  field: FieldAtom
}

export interface FieldSetValidation {
  /** The list of field validation errors. */
  errors: FieldSetFieldError[]

  /** The validation actuality status. */
  triggered: boolean

  /** The field async validation status */
  validating: undefined | Promise<{ errors: FieldSetFieldError[] }>
}

export interface ValidationlessFieldSet<
  T extends FormInitState,
> extends Computed<FormState<T>> {
  /** Fields from the init state */
  fields: FormFields<T>

  /** Computed list of all the fields from the fields tree */
  fieldsList: Computed<FieldAtom[]>

  /** Computed list of all the field arrays from the fields tree */
  fieldArraysList: Computed<FormFieldArrayAtom[]>

  /**
   * Atom with focus state of the fieldset, computed from all the fields in
   * `fieldsList`
   */
  focus: Computed<FieldFocus>

  /** Action to set initial values for each field or field array in the fieldset */
  init: Action<[initState: FormPartialState<T>], void>

  /** Action to reset the state, the value, the validation, and the focus states. */
  reset: Action<[initState?: FormPartialState<T>], void>
}

export interface FieldSet<
  T extends FormInitState,
> extends ValidationlessFieldSet<T> {
  /**
   * Atom with validation state of the fieldset, computed from all the fields in
   * `fieldsList`
   */
  validation: Computed<FieldSetValidation> & {
    /** Action to trigger fieldset validation. */
    trigger: Action<[], FieldSetValidation> & AbortExt
  }
}

export const reatomFieldSet = <T extends FormInitState>(
  fields: FormFields<T>,
  name: string = named('fieldSet'),
): FieldSet<T> => {
  const fieldsList = computed(
    () => computeFieldsList(fields),
    `${name}._fieldsList`,
  )
  const fieldArraysList = computed(
    () => computeFieldArraysList(fields),
    `${name}._fieldArraysList`,
  )
  const fieldsState = computed(() => deatomize(fields), name)

  const focus = computed(() => {
    const focus = { ...fieldInitFocus }

    for (const field of fieldsList()) {
      if (field.disabled()) continue

      const { active, dirty, touched } = field.focus()
      focus.active ||= active
      focus.dirty ||= dirty
      focus.touched ||= touched
    }

    return focus
  }, `${name}.focus`).extend(withMemo())

  const validation = computed(() => {
    const validationErrors: FieldSetFieldError[] = []
    const promises: Promise<{ errors: FieldSetFieldError[] }>[] = []
    const validation: FieldSetValidation = {
      errors: validationErrors,
      validating: undefined,
      triggered: true,
    }

    for (const field of fieldsList()) {
      if (field.disabled()) continue

      const errors = field.validation.errors()
      const { triggered, validating } = field.validation()

      validation.triggered &&= triggered
      validationErrors.push(...errors.map((err) => ({ ...err, field })))

      if (validating)
        promises.push(
          validating.then(({ errors }) => ({
            errors: errors.map((err) => ({ ...err, field })),
          })),
        )
    }

    validation.validating = promises.length
      ? Promise.all(promises).then((results) => ({
          errors: results.flatMap((e) => e.errors),
        }))
      : undefined

    return validation
  }, `${name}.validation`).extend(withMemo(), (target) => ({
    trigger: action(() => {
      for (const field of fieldsList()) {
        if (!field.validation().triggered) field.validation.trigger()
      }

      return target()
    }, `${target.name}.trigger`).extend(withAbort()),
  }))

  const reinitState = (initState: FormPartialState<T>, fields: FormFields) => {
    for (const [key, value] of Object.entries(initState as Rec)) {
      const keyValue = fields[key] as unknown
      if (isLinkedListAtom(keyValue)) {
        // @ts-ignore
        keyValue.initState.set(
          // @ts-ignore
          keyValue.initiateFromSnapshot(value.map((v) => [v])),
        )
      } else if (
        isObject(value) &&
        !(value instanceof Date) &&
        key in fields &&
        !isAtom(keyValue)
      ) {
        reinitState(value, keyValue as unknown as FormFields)
      } else if (isAtom(keyValue)) {
        // @ts-ignore
        keyValue.initState.set(value)
      }
    }
  }

  const init = action((initState: FormPartialState<T>) => {
    reinitState(initState, fields)
  }, `${name}.init`)

  const reset = action((initState?: FormPartialState<T>) => {
    if (initState) init(initState)

    fieldArraysList().forEach((fieldArray) => fieldArray.reset())
    fieldsList().forEach((fieldAtom) => fieldAtom.reset())
  }, `${name}.reset`)

  return Object.assign(fieldsState, {
    fields,
    fieldsList,
    fieldArraysList,
    focus,
    validation,
    init,
    reset,
  })
}

const computeFieldArraysList = <T extends FormInitState>(
  fields: FormFields<T>,
  acc: Array<FormFieldArrayAtom<unknown>> = [],
) => {
  const computeElement = (
    element: FormFieldElement,
    acc: Array<FormFieldArrayAtom<unknown>> = [],
  ) => {
    if (isLinkedListAtom(element)) {
      acc.push(element as FormFieldArrayAtom<unknown>)
      // @ts-ignore
      element.array().forEach((e) => computeElement(e, acc))
    } else if (!isAtom(element)) computeFieldArraysList(element, acc)

    return acc
  }

  for (const [_, field] of entries(fields)) {
    acc.push(...computeElement(field))
  }

  return acc
}

const computeFieldsList = <T extends FormInitState>(
  fields: FormFields<T>,
  acc: Array<FieldAtom> = [],
): Array<FieldAtom> => {
  const computeElement = (
    element: FormFieldElement,
    acc: Array<FieldAtom> = [],
  ) => {
    if (isLinkedListAtom(element)) {
      const elements = element.array()
      elements.forEach((e) => computeElement(e, acc))
    } else if (isFieldAtom(element)) acc.push(element)
    else if (isObject(element)) computeFieldsList(element, acc)

    return acc
  }

  for (const [_, field] of entries(fields)) acc.push(...computeElement(field))

  return acc
}
